package freakin.wify;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.wifi.ScanResult;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

/**
 * NetworkManager adds new networks to the database, find nearest (open)
 * network.
 * 
 */
public class NetworkManager {

	public static final String TAG = "NetworkManager";

	DbHelper database;

	List<Network> networksToSend;

	// used for saving to the database only.
	List<Network> netsToSave;

	TimeKeeper tk;

	Context context;

	Locationer locationer;

	/** true = started, false = stopped **/
	public boolean state;

	WifiApp app;

	public NetworkManager(Context context, DbHelper database, WifiApp app) {

		networksToSend = new ArrayList<Network>();
		netsToSave = new ArrayList<Network>();

		this.context = context;

		this.app = app;

		locationer = new Locationer();

		this.database = database;
	}

	public void start() {
		startGPStracking();
		tk = new TimeKeeper();
		tk.start();
		state = true;
	}

	public void stop() {
		stopGPStracking();
		tk.interrupt();
		tk = null;
		state = false;
	}

	public void feedNewScanResults(List<ScanResult> results) {
		if (!state) {
			start();
		}

		locationer.updateLastKnownLocation();

		Log.d(TAG, "received wifi scan results");

		Network n;

		for (ScanResult result : results) {
			if (isNew(result)) {
				n = new Network(result);
				n.latitude = locationer.lat;
				n.longitude = locationer.lon;
				n.locationSet = true;

				n.open = isNetworkOpen(result);

				networksToSend.add(n);
				netsToSave.add(n);

				Log.d(TAG, "Found Network! " + result.toString());
			}
		}
		database.insertNetworks(netsToSave);
		netsToSave.clear();

	}

	private void startGPStracking() {
		LocationManager mlocManager = (LocationManager) context
				.getSystemService(Context.LOCATION_SERVICE);

		/*
		 * More info:
		 * http://developer.android.com/guide/topics/location/obtaining
		 * -user-location.html
		 */

		mlocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0,
				locationer);
		mlocManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0,
				0, locationer);

		Log.d(TAG, "started GPS tracking");
	}

	private void stopGPStracking() {
		LocationManager mlocManager = (LocationManager) context
				.getSystemService(Context.LOCATION_SERVICE);
		mlocManager.removeUpdates(locationer);
		Log.d(TAG, "stopped GPS tracking");
	}

	private boolean isNew(ScanResult result) {
		if (database.searchForNetwork(result.BSSID, null) > 0) {
			return false;
		} else
			return true;
	}

	private void sendResults() {
		Log.d(TAG, "sending results to the website");

		// TODO check if we have internet connection
		if (!networksToSend.isEmpty()) {
			Sender sender = new Sender();
			sender.transferResultsToSend(networksToSend);
			sender.start();

			networksToSend = new ArrayList<Network>();
		}

		// TODO send some data from the db.

	}

	/**
	 * Finds a network that is closes to the current location.
	 * 
	 * @param only_open
	 *            search only for open networks?
	 * @return Network closes to the current location. or null, if network could
	 *         not be found.
	 */
	public Network getNearestNetwork(boolean only_open) {

		Log.d(TAG, "looking for a nearest network...");

		locationer.updateLastKnownLocation();

		// imaginary network, for computing distances:
		Network im = new Network();
		im.latitude = locationer.lat;
		im.longitude = locationer.lon;
		im.locationSet = true;

		// best is the worst at first
		Network best = null;
		double bestDist = 99999999;
		double tmpDist;

		// Kod zwracajacy cala baze jest srednio fajny
		// i bardzo niewydajny... Moze szukac sieci tylko
		// poprzez zapytanie do serwera? To by bylo dobre...
		// ale obciazaloby serwer przy duzej liczbie userow.
		Set<Network> networksDB = database.getNetworksWithLocation(only_open);
		if (networksDB != null) {
			Log.d(TAG, "looking for the nearest network in the database");
			// then look in the db first
			for (Network netDB : networksDB) {
				tmpDist = Network.getDistance(im, netDB);

				if (tmpDist == -1)
					continue;
				else if (tmpDist < bestDist) {
					best = netDB;
					bestDist = tmpDist;
				}
			}
		} else {
			Log.d(TAG, "no networks in the database...");
		}

		// TODO:
		// - get networks from webservice OR:
		// - send a network AND current location to the webservice,
		// so that it could find closes network for us.

		if (best == null) {
			app.nearestNetwork = null;
			return null;
		}

		best = database.getNetwork(best.bssid, null);

		Log.d(TAG, "searching for the closest network on the serwer...");

		SoapHelper sh = new SoapHelper();
		if (Utils.isConnected(context)) {
			try {
				Network fromWS = sh.getClosestOpenNetwork(locationer.lat, locationer.lon);
				if (fromWS != null && fromWS.locationSet) {
					Log.d(TAG, "got network from the webservice.");
					if (best == null || (Network.getDistance(best, fromWS) < bestDist)) {
						Log.d(TAG, "network from webservice is better");
						app.nearestNetwork = fromWS;
						return fromWS;
					}
				}
			} catch (Exception e) {
				Log.e(TAG, "exception when getting network from the webservice");
				e.printStackTrace();
			}
		}

		app.nearestNetwork = best;

		return best;
	}

	private boolean isNetworkOpen(ScanResult scanResult) {
		final String cap = scanResult.capabilities;
		final String[] securityModes = { "WEP", "PSK", "EAP" };
		for (int i = securityModes.length - 1; i >= 0; i--) {
			if (cap.contains(securityModes[i])) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Deletes all networks saved on this device from the webservice. Used ONLY
	 * FOR DEBUGGING :)
	 * 
	 * @return true, if deletion completed successfully, false if could not
	 *         delete nets for any reason.
	 */
	public boolean deleteNetworksFromWebservice() {
		if (!Utils.isConnected(context))
			return false;

		Log.d(TAG, "deleting networks from webservice");

		int numAll = 0;
		int numDel = 0;

		SoapHelper sh = new SoapHelper();

		Set<Network> networks22 = database.getNetworksWithLocation(false);
		for (Network net : networks22) {
			numAll++;
			if (sh.removeNetwork(net.bssid)) {
				database.setNetworkAsSend(net, false);
				numDel++;
			}
		}
		
		Toast.makeText(context, "deleted " + numDel + " of " + numAll
				+ " networks from the webservice" , Toast.LENGTH_SHORT);

		Log.d(TAG, "deleted " + numDel + " of " + numAll
				+ " networks from the webservice");

		return true;
	}

	private class Sender extends Thread {

		List<Network> networks;

		private static final String TAG = "NetworkManager.Sender";

		public Sender() {
			super("WiFiScanReceiver-Sender");
			Log.d(TAG, "created sender.");
			networks = null;
		}

		public void transferResultsToSend(List<Network> nets) {
			networks = nets;
		}

		public void run() {
			if (networks.isEmpty()) {
				Log.d(TAG, "No results to send... Exiting");
				return;
			}

			// TODO!
			// Odkomentowac wysylanie dopiero jak serwer bedzie mial mozliwosc
			// potwierdzenia, ze otrzymal dane.
			if (Utils.isConnected(context)) {
				try {
					Log.d(TAG, "attempting to send data to the server");
					SoapHelper sh = new SoapHelper();
					for (Network ap : networks) {
						/*
						if (sh.uploadAp(ap.bssid, ap.ssid, ap.frequency, ap.level,
						ap.security, ap.latitude, ap.longitude)) {
							database.setNetworkAsSend(ap, true);
						}
						*/
					}
				} catch (Exception e) {
					Log.e(TAG, "uploading data to webservice failed!");
					e.printStackTrace();
				}
			} else {
				Log.d(TAG,
						"data upload attempt canceled - device not connected");
			}
		}
	}

	private class TimeKeeper extends Thread {

		private static final String TAG = "NetworkManager.TimeKeeper";

		public TimeKeeper() {

		}

		public void run() {
			try {
				while (true) {
					Log.d(TAG,
							"TimeKeeper wakes up... Is there data to send? = "
									+ !networksToSend.isEmpty());
					if (!networksToSend.isEmpty()) {
						Log.d(TAG, "sending results to the webservice");
						sendResults();
					}

					Log.d(TAG, "TimeKeeper goes to sleep... for "
							+ app.sendInterval + " ms");
					Thread.sleep(app.sendInterval);
				}
			} catch (InterruptedException ie) {
				Log.d(TAG, "interrupted");
				ie.printStackTrace();
			}
		}
	}

	class Locationer implements LocationListener {

		private static final String TAG = "NetworkManager.Locationer";

		public double lat;
		public double lon;

		/** true, if GPS is enabled **/
		public boolean statusGPS;

		public Locationer() {
			lat = 0;
			lon = 0;
			statusGPS = false;
		}

		private void updateLocations(Location loc) {
			lat = loc.getLatitude();
			lon = loc.getLongitude();
		}

		public void updateLastKnownLocation() {
			LocationManager locationManager = (LocationManager) context
					.getSystemService(Context.LOCATION_SERVICE);

			Location loc = null;

			// check: is GPS working? Use NETWORK_PROVIDER if not.
			if (statusGPS) {
				loc = locationManager
						.getLastKnownLocation(LocationManager.GPS_PROVIDER);
			} else {
				loc = locationManager
						.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
			}

			if (loc != null) {
				updateLocations(loc);
				Log.d(TAG, "updating location... Using GPS? = " + statusGPS
						+ ". lat: " + lat + ", lon: " + lon);
			} else {
				Log.e(TAG,
						"updateLastKnownLocation(..) gave loc == null. statusGPS == "
								+ statusGPS);
			}

		}

		public void onLocationChanged(Location loc) {

			updateLocations(loc);

			Log.d(TAG, "Location changed. lat: " + lat + "... lon: " + lon);

		}

		public void onProviderDisabled(String provider) {
			if (provider.equalsIgnoreCase(LocationManager.GPS_PROVIDER))
				statusGPS = false;
			Log.d(TAG, "disabled location provider. " + provider);

		}

		public void onProviderEnabled(String provider) {
			if (provider.equalsIgnoreCase(LocationManager.GPS_PROVIDER))
				statusGPS = true;
			Log.d(TAG, "enabled location provider. " + provider);

		}

		public void onStatusChanged(String provider, int status, Bundle extras) {

		}

	}

}
